package com.behelpful.common.integration.iredis.support;

import cn.hutool.core.util.StrUtil;
import com.behelpful.common.core.result.FailedResult;
import com.behelpful.common.core.result.OkResult;
import com.behelpful.common.core.utils.ICollUtil;
import com.behelpful.common.core.utils.IDateUtil;
import com.behelpful.common.integration.iredis.constants.ValidateCodeEnum;
import com.behelpful.common.integration.iredis.constants.RedisLockEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.ShardedJedis;
import redis.clients.jedis.ShardedJedisPool;

import java.time.LocalDate;

/**
 * 这是一个Redis的工具类
 *
 * @author Alay
 * @date 2021-05-02 22:52
 * @project behelpful
 */
@Slf4j
@Component
public class IRedisUtil {

    private static final String LOCK = "LOCK";
    private static volatile ThreadLocal<Jedis> currentJedis = new ThreadLocal<>();
    private static volatile ThreadLocal<ShardedJedis> currentShardJedis = new ThreadLocal<>();

    @Autowired
    private JedisPool jedisPool;
    @Autowired
    private ShardedJedisPool shardedJedisPool;

    public Jedis getJedis() {
        Jedis jedis = jedisPool.getResource();
        jedis.select(0);
        return jedis;
    }


    public ShardedJedis shardedJedis() {
        ShardedJedis shardedJedis = shardedJedisPool.getResource();
        return shardedJedis;
    }


    public void close(Jedis jedis) {
        if (null != jedis) {
            jedis.close();
        }
    }

    public void close(ShardedJedis shardedJedis) {
        if (null != shardedJedis) {
            shardedJedis.close();
        }
    }

    /**
     * 此函数慎用
     *
     * @return
     */
    public Jedis threadJedis() {
        Jedis jedis = currentJedis.get();
        if (null == jedis) {
            jedis = this.getJedis();
            currentJedis.set(jedis);
        }
        return jedis;
    }

    public void closeJedis() {
        try {
            this.close(currentJedis.get());
        } finally {
            // 只会删除自己线程中的数据
            currentJedis.remove();
        }
    }


    /**
     * 此函数慎用
     *
     * @return
     */
    public ShardedJedis threadShardJedis() {
        ShardedJedis shardedJedis = currentShardJedis.get();
        if (null == shardedJedis) {
            shardedJedis = this.shardedJedis();
            currentShardJedis.set(shardedJedis);
        }
        return shardedJedis;
    }

    public void closeShardJedis() {
        try {
            this.close(currentShardJedis.get());
        } finally {
            // 只会删除自己线程中的数据
            currentShardJedis.remove();
        }
    }


    /**
     * 尝试锁,首先获取当前是否有锁，无锁则加锁
     *
     * @param redisLock
     * @param suffix
     * @return 返回之前状态是否被锁主状态
     */
    public Boolean tryLock(RedisLockEnum redisLock, String suffix) {
        Boolean hasLock;
        Jedis jedis = null;
        try {
            jedis = this.getJedis();
            jedis.select(redisLock.getIndex());
            hasLock = jedis.exists(redisLock.getKey() + suffix);
            if (!hasLock) {
                jedis.setex(redisLock.getKey() + suffix, redisLock.getExpireSec(), LOCK);
            }
        } finally {
            this.close(jedis);
        }
        return hasLock;
    }


    /**
     * 是否存在锁
     *
     * @param redisLock
     * @param suffix
     * @return
     */
    public Boolean hasLock(RedisLockEnum redisLock, String suffix) {
        Boolean exists;
        Jedis jedis = null;
        try {
            jedis = this.getJedis();
            jedis.select(redisLock.getIndex());
            exists = jedis.exists(redisLock.getKey() + suffix);
        } finally {
            this.close(jedis);
        }
        return exists;
    }


    /**
     * 防止重复锁/频率限定锁
     *
     * @param redisLock 锁枚举类对象
     * @param suffix    后缀(区分开各个锁,如 userId 进行区分)
     */
    public void lock(RedisLockEnum redisLock, String suffix) {
        Jedis jedis = null;
        try {
            jedis = this.getJedis();
            jedis.select(redisLock.getIndex());
            jedis.setex(redisLock.getKey() + suffix, redisLock.getExpireSec(), LOCK);
        } finally {
            this.close(jedis);
        }
    }


    /**
     * 解锁,释放锁
     *
     * @param redisLock
     * @param suffix
     */
    public void unLock(RedisLockEnum redisLock, String suffix) {
        Jedis jedis = null;
        try {
            jedis = this.getJedis();
            jedis.select(redisLock.getIndex());
            jedis.del(redisLock.getKey() + suffix);
        } finally {
            this.close(jedis);
        }
    }


    /**
     * 验证码发送前校验(校验验证码是否存在 / 是否达到当日上限)
     *
     * @param validateCode
     * @param codeId
     * @return
     */
    public String validateCodeSendBefore(ValidateCodeEnum validateCode, String codeId) {
        Jedis jedis = null;
        try {
            jedis = this.getJedis();
            jedis.select(validateCode.getIndex());

            // 检测验证码是否过期,
            String codeKey = validateCode.getKey() + codeId;
            String oldCode = jedis.get(codeKey);
            if (StrUtil.isNotEmpty(oldCode)) {
                log.warn("验证码未过期:{}，{}", codeId, oldCode);
                return FailedResult.TOO_FREQUENT.getMessage();
            }

            // 验证该手机号码发送验证码是否达到上限
            String countStr = jedis.hget(validateCode.getLimitKey(), codeId);
            if (null != countStr && validateCode.getLimit() <= Integer.valueOf(countStr)) {
                // 当日次数达到上限
                log.warn("今日验证码次数达到上限:{}", codeId);
                return FailedResult.DAY_COUNT_CEILING.getMessage();
            }
            return OkResult.SUCCESS.getMessage();
        } finally {
            this.close(jedis);
        }
    }

    /**
     * 保存验证码
     *
     * @param validateCode 样正码枚举
     * @param codeId       验证码ID
     * @param code         验证码
     * @param dayLimit     是否日限定（适用于电话/或者userId）
     * @return
     */
    public String saveValidateCode(ValidateCodeEnum validateCode, String codeId, String code, boolean dayLimit) {
        Jedis jedis = null;
        String isOk;
        try {
            jedis = this.getJedis();
            jedis.select(validateCode.getIndex());

            // 验证码存入缓存中
            String codeKey = validateCode.getKey() + codeId;
            isOk = jedis.setex(codeKey, validateCode.getExpireSecond(), code);

            // 是否需要加入日限定
            if (dayLimit) {
                // 该手机号码发送验证码的次数进行 ++ 操作
                String countStr = jedis.hget(validateCode.getLimitKey(), codeId);
                // 已使用发送验证码的次数
                Integer count = 0;
                if (null != countStr) {
                    // 今日首次发送验证码
                    count = Integer.valueOf(countStr);
                }
                ++count;

                // 验证码限制存入缓存
                jedis.hset(validateCode.getLimitKey(), codeId, count.toString());

                Long ttl = jedis.ttl(validateCode.getLimitKey());
                // 当 key 不存在时，返回 -2 。 当 key 存在但没有设置剩余生存时间时，返回 -1 。 否则，以毫秒为单位，返回 key 的剩余生存时间。
                if (ttl < 0) {
                    // 还未设置过期时间
                    // 设置限制当天过期
                    jedis.expireAt(validateCode.getLimitKey(), IDateUtil.stampSecond(IDateUtil.dayEnd(LocalDate.now())));
                }
            }
        } finally {
            this.close(jedis);
        }
        return isOk;
    }

    /**
     * 验证码验证
     *
     * @param codeId 验证码唯一ID(可以是手机号/可以是userId,可以是UUID)
     * @param code   用户输入的验证码
     * @return
     */
    public boolean verifyValidateCode(ValidateCodeEnum validateCode, String codeId, String code) {
        Long eval;
        Jedis jedis = null;
        try {
            jedis = this.getJedis();
            // Redis 第一个库太拥挤,不方便查看,所以放第二个库中
            jedis.select(validateCode.getIndex());
            String key = validateCode.getKey() + codeId;
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            eval = (Long) jedis.eval(script, ICollUtil.asList(key), ICollUtil.asList(code));
        } finally {
            this.close(jedis);
        }
        boolean isOk = eval != 0;
        if (!isOk) {
            log.error("codeId :{},验证码:{}不合法", codeId, code);
        }
        return isOk;
    }
}